#include <iostream>
#include "Types.hpp"
#include "Debug.hpp"
#include "ClWrapper.hpp"
#include "DeviceWrapper.hpp"
#include "FileReader.hpp"
#include "VTKPathWriter.hpp"
#include <vector>
#include <fstream>
#include <string>
#include <cmath>
#include <iomanip>


int main(int argc, char **argv)
{

	if(argc!=2){
		ERROR("Please Type the name of the input file");
		return 1;
	}
	std::string filename=argv[1];
	INFOP("");

	//TESTING READER
	FileReader reader=FileReader();

	//redistering keys
	reader.RegisterIntParameter(p_PART_OUT_FREQ,v_PART_OUT_FREQ);
	reader.RegisterIntParameter(p_VTK_OUT_FREQ,v_VTK_OUT_FREQ);
	reader.RegisterIntParameter(p_CL_WORKGROUP_SIZE,v_CL_WORKGROUP_SIZE);

	reader.RegisterDoubleParameter(p_TIME_STEP,v_TIME_STEP);
	reader.RegisterDoubleParameter(p_END_TIME,v_END_TIME);
	reader.RegisterDoubleParameter(p_EPSILON,v_EPSILON);
	reader.RegisterDoubleParameter(p_SIGMA,v_SIGMA);

	reader.RegisterStringParameter(p_INPUT_FILE,v_INPUT_FILE);
	reader.RegisterStringParameter(p_PART_OUT_NAME,v_PART_OUT_NAME);
	reader.RegisterStringParameter(p_VTK_OUT_NAME,v_VTK_OUT_NAME);

	//Parsing parameters
	reader.ReadFile(filename);
	cl_real epsilon=reader.GetDoubleParameter(p_EPSILON);
	cl_real sigma=reader.GetDoubleParameter(p_SIGMA);
	cl_real sigma6=pow(sigma,6);
	cl_uint WORK_GROUP_SIZE=reader.GetIntParameter(p_CL_WORKGROUP_SIZE);
	cl_real delta_t=reader.GetDoubleParameter(p_TIME_STEP);
	cl_real end_t=reader.GetDoubleParameter(p_END_TIME);
	int vtkFreq = reader.GetIntParameter(p_VTK_OUT_FREQ);
	int partfreq = reader.GetIntParameter(p_PART_OUT_FREQ);
	std::string part_out_name= reader.GetStringParameter(p_PART_OUT_NAME);
	std::string vtk_out_name= reader.GetStringParameter(p_VTK_OUT_NAME);

	// Getting information about the Platforms
	ClWrapper platofrmCL;
	std::cout << "Available Platforms" << std::endl;
	std::cout << platofrmCL;

	// Get the Platform from the USER
	cl::Platform pl = platofrmCL.getPlatform();	
	std::cout << "Using the Following Platform: " << std::endl;
	ClWrapper platform_sel(pl);
	std::cout << platform_sel;


	// Obtain information about the Devices present in the Selected Platform
	std::cout << "Available Devices" << std::endl;
	DeviceWrapper deviceCL(pl);
	std::cout << deviceCL;

	cl::Device device;      
	cl::Context context; 
	cl::CommandQueue queue; 
	cl::Program::Sources sources;
	cl::Program program;

	//Set the Device, Context, Program, Commandqueue, sources. Can be modified later to make number of Programs instead of one program.                                                                  
	deviceCL.getDeviceProgram("a2_kernels.cl", device, context, queue, sources, program);
	std::cout << "Using the Following Device: " << std::endl;
	DeviceWrapper deviceSel(device);
	std::cout << deviceSel;

	//reader.PrintParameters(std::cout);
	cl_uint N;
	cl_real* masses;
	cl_real4* positions;
	cl_real4* velocities;
	std::string fileParti=reader.GetStringParameter(p_INPUT_FILE);
	reader.readParticleFile(N,masses,positions,velocities,fileParti);
	cl_real4* oldForces=new cl_real4[N];
	cl_real4* forces=new cl_real4[N];
	//reader.PrintParameters(std::cout);
	//Print the values from the file
	/*std::cout<<"\nINITIAL VALUES\nMasses\tPositions\tVelocities\n";
	for(cl_uint i=0;i<N;++i)
	{
	std::cout<<masses[i]<<"  "<<positions[i].s[0]<<"  "<<positions[i].s[1]<<"  "<<positions[i].s[2]<<"  "
	<<velocities[i].s[0]<<"  "<<velocities[i].s[1]<<"  "<<velocities[i].s[2]<<"\n";
	}*/

	try
	{
		/********MEMORY OBJECTS********/
		//Positions buffer
		cl::Buffer posBuffer=cl::Buffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR ,sizeof(cl_real4) * N, &positions[0]);
		//Velocities buffer
		cl::Buffer velBuffer=cl::Buffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR ,sizeof(cl_real4) * N, &velocities[0]);
		//Force buffers
		cl::Buffer forceBuffer = cl::Buffer(context, CL_MEM_ALLOC_HOST_PTR, sizeof(cl_real4) * N);
		cl::Buffer oldForceBuffer = cl::Buffer(context, CL_MEM_ALLOC_HOST_PTR, sizeof(cl_real4) * N);
		//(1/2m) buffer of masses
		cl::Buffer inv2MassBuffer = cl::Buffer(context, CL_MEM_ALLOC_HOST_PTR, sizeof(cl_real) * N);
		cl_real* inv_2masses = (cl_real*)queue.enqueueMapBuffer(inv2MassBuffer, true, CL_MAP_WRITE, 0,N * sizeof(cl_real));
		for(cl_uint i=0;i<N;++i)
			inv_2masses[i]=1/(2*masses[i]);
		queue.enqueueUnmapMemObject(inv2MassBuffer, (void*)inv_2masses);


		/*********KERNELS**************/
		//compute the forces
		cl::Kernel kCalcForces = cl::Kernel(program, "forceCalculation");
		kCalcForces.setArg(0, posBuffer);
		kCalcForces.setArg(1, forceBuffer);
		kCalcForces.setArg(2, cl::__local(sizeof(cl_real4) * WORK_GROUP_SIZE));
		kCalcForces.setArg(3, epsilon);
		kCalcForces.setArg(4, sigma6);
		kCalcForces.setArg(5, N);

		//Update positions and copy forces to oldForces
		cl::Kernel kUpdatePositions = cl::Kernel(program, "updatePositions");
		kUpdatePositions.setArg(0, posBuffer);
		kUpdatePositions.setArg(1, oldForceBuffer);
		kUpdatePositions.setArg(2, forceBuffer);
		kUpdatePositions.setArg(3, velBuffer);
		kUpdatePositions.setArg(4, inv2MassBuffer);
		kUpdatePositions.setArg(5, delta_t);
		kUpdatePositions.setArg(6, N);

		//Update velocities
		cl::Kernel kUpdateVelocities = cl::Kernel(program, "updateVelocities");
		kUpdateVelocities.setArg(0, velBuffer);
		kUpdateVelocities.setArg(1, oldForceBuffer);
		kUpdateVelocities.setArg(2, forceBuffer);
		kUpdateVelocities.setArg(3, inv2MassBuffer);
		kUpdateVelocities.setArg(4, delta_t);
		kUpdateVelocities.setArg(5, N);
		/******************SIMULATION********************/
		//Calculate the initial forces
		queue.enqueueNDRangeKernel(kCalcForces, cl::NullRange, cl::NDRange(WORK_GROUP_SIZE), cl::NDRange(1));
		int counter=0,n_print=0,p_print=0;
		/*for(cl_int i(0);i<vtkFreq;++i){
		queue.enqueueNDRangeKernel(kUpdatePositions, cl::NullRange, cl::NDRange(WORK_GROUP_SIZE), cl::NDRange(1));
		queue.enqueueNDRangeKernel(kCalcForces, cl::NullRange, cl::NDRange(WORK_GROUP_SIZE), cl::NDRange(1));
		queue.enqueueNDRangeKernel(kUpdateVelocities, cl::NullRange, cl::NDRange(WORK_GROUP_SIZE), cl::NDRange(1));
		}
		queue.enqueueReadBuffer(oldForceBuffer, CL_TRUE, 0, sizeof(cl_real4) * N, oldForces);
		queue.enqueueReadBuffer(forceBuffer, CL_TRUE, 0, sizeof(cl_real4) * N, forces);
		queue.enqueueReadBuffer(posBuffer, CL_TRUE, 0, sizeof(cl_real4) * N, positions);
		queue.enqueueReadBuffer(velBuffer, CL_TRUE, 0, sizeof(cl_real4) * N, velocities);
		std::cout<<"\n\nPositions\tOldForces\tForces\tVelocities\n\n";
		for(cl_uint i(0);i<N;++i){
		std::cout<<positions[i].s[0]<<"  "<<positions[i].s[1]<<"  "<<positions[i].s[2]<<"\t";
		std::cout<<oldForces[i].s[0]<<"  "<<oldForces[i].s[1]<<"  "<<oldForces[i].s[2]<<"\t";
		std::cout<<forces[i].s[0]<<"  "<<forces[i].s[1]<<"  "<<forces[i].s[2]<<"\t";
		std::cout<<velocities[i].s[0]<<"  "<<velocities[i].s[1]<<"  "<<velocities[i].s[2]<<"\n";
		}*/




		for(cl_real t=0;t<end_t;t+=delta_t)
		{
			if(counter%partfreq==0)			
			{
				queue.enqueueReadBuffer(posBuffer, CL_TRUE, 0, sizeof(cl_real4) * N, positions);
				queue.enqueueReadBuffer(velBuffer, CL_TRUE, 0, sizeof(cl_real4) * N, velocities);
				std::string filename("vtkFiles//");
				filename+=part_out_name;				
				filename+=std::to_string(p_print);
				filename += ".txt";
				std::cout << filename;
				std::ofstream mStream;
				mStream.open(filename);
				mStream << N<<"\n";

				for(cl_uint i(0);i<N;++i){
					mStream<<std::fixed;
					mStream<< std::setprecision(6) << masses[i] << "  "<< positions[i].s[0]<<"  "<<positions[i].s[1]<<"  "<<positions[i].s[2]<<"\t";					
					mStream<<velocities[i].s[0]<<"  "<<velocities[i].s[1]<<"  "<<velocities[i].s[2]<<"\n";
				}
				++p_print;
				mStream.close();			  
			}

			if(counter%vtkFreq==0)
			{
				queue.enqueueReadBuffer(posBuffer, CL_TRUE, 0, sizeof(cl_real4) * N, positions);
				queue.enqueueReadBuffer(velBuffer, CL_TRUE, 0, sizeof(cl_real4) * N, velocities);
				std::string filename("vtkFiles//");
				filename+=vtk_out_name;
				filename+=std::to_string(n_print);
				VTKPathWriter writer(filename,N);
				writer.writePoints(positions);
				writer.writeScalars(masses,"m");
				writer.writeVector(velocities,"v");
				++n_print;
			}
			++counter;
			queue.enqueueNDRangeKernel(kUpdatePositions, cl::NullRange, cl::NDRange(WORK_GROUP_SIZE), cl::NDRange(1));
			queue.enqueueNDRangeKernel(kCalcForces, cl::NullRange, cl::NDRange(WORK_GROUP_SIZE), cl::NDRange(1));
			queue.enqueueNDRangeKernel(kUpdateVelocities, cl::NullRange, cl::NDRange(WORK_GROUP_SIZE), cl::NDRange(1));
		}
		queue.finish();
	}
	catch(cl::Error er) 
	{
		printf("ERROR: %s(%d)\n", er.what(), er.err());
	}

	delete [] masses;
	delete [] positions;
	delete [] velocities;
	delete [] forces;
	delete [] oldForces;
	return 0;
}


